{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "6cc3f218-743a-4ea2-8e5d-8dfe838fd875",
   "metadata": {},
   "source": [
    "# Paddle2ONNX Technology Walkthrough\n",
    "这篇文章简要的介绍 [Paddle2ONNX](https://github.com/PaddlePaddle/Paddle2ONNX) 背后的技术， 希望能对理解 Paddle2ONNX 项目有一定帮助。你可以下载之后在 Jupyter Notebook 里来运行这里面的代码。（请提前安装好环境）\n",
    "\n",
    "## 回顾一下 Paddle2ONNX 是做什么的\n",
    "Pddle2ONNX 项目的定位是把一个 PaddlePaddle 格式的模型转换成一个 ONNX 格式的模型。 为了能更清晰的展示，它在做什么，我们先准备一个简单的 PaddlePaddle 的模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "4e1eac81-d77e-47fe-a0d0-67c14661aa20",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "WARNING: Logging before InitGoogleLogging() is written to STDERR\n",
      "I0625 19:30:26.651901 79480192 kernel_dispatch.h:102] Get BackendSet from tensor\n",
      "I0625 19:30:26.652213 79480192 kernel_dispatch.h:102] Get BackendSet from tensor\n",
      "I0625 19:30:26.794221 79480192 program_interpreter.cc:243] New Executor is Running.\n"
     ]
    }
   ],
   "source": [
    "import paddle\n",
    "import paddle.nn.functional as F\n",
    "\n",
    "class MyModel(paddle.nn.Layer):\n",
    "    def __init__(self, input_size, hidden_size):\n",
    "        super().__init__()\n",
    "        self.linear1 = paddle.nn.Linear(input_size, hidden_size)\n",
    "        self.linear2 = paddle.nn.Linear(hidden_size, 1)\n",
    "\n",
    "    def forward(self, inputs):\n",
    "        x = self.linear1(inputs)\n",
    "        x = F.sigmoid(x)\n",
    "        x = self.linear2(x)\n",
    "        return x\n",
    "\n",
    "input_size, hidden_size = 8, 4\n",
    "model = MyModel(input_size, hidden_size)\n",
    "\n",
    "x_input_spec = paddle.static.InputSpec([None, input_size], 'float32', 'x')\n",
    "paddle.jit.save(model, \"./demo\", input_spec=[x_input_spec])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e6c3ffa-cd59-4159-ab95-2389d00f3c9e",
   "metadata": {},
   "source": [
    "上面的代码将一个 PaddlePaddle 的模型存储了下来。 请注意，这里只是为了示意，所以忽略了模型的训练的部分。 接下来，我们用 Paddle2ONNX 把 PaddlePaddle 的模型转换成 ONNX 格式的模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "a3c88083-15d1-437d-9152-07101a6ac6fb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[Paddle2ONNX] Start to parse PaddlePaddle model...\n",
      "[Paddle2ONNX] Model file path: ./demo.pdmodel\n",
      "[Paddle2ONNX] Parameters file path: ./demo.pdiparams\n",
      "[Paddle2ONNX] Start to parsing Paddle model...\n",
      "[Paddle2ONNX] Use opset_version = 11 for ONNX export.\n",
      "[Paddle2ONNX] PaddlePaddle model is exported as ONNX format now.\n"
     ]
    }
   ],
   "source": [
    "import paddle2onnx\n",
    "\n",
    "paddle2onnx.export(\n",
    "        model_file='./demo.pdmodel',\n",
    "        params_file='./demo.pdiparams',\n",
    "        save_file='./demo.onnx'\n",
    "        )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cfaba22f-ef51-4674-ac0d-71ceb495bfe5",
   "metadata": {},
   "source": [
    "运行完上面的代码后，会多出来一个 demo.onnx 的文件，这就是转换后的 ONNX 格式的模型文件。 BTW： 你也可以用 `paddle2onnx` 这个命令行工具来做到同样的事情，跟上面的代码本质上是一样的。 到此为止，硬盘上会多出来下面的 4 个文件。\n",
    "- demo.pdmodel ：  这个文件存储的是 PaddlePaddle 模型的模型结构\n",
    "- demo.pdiparams ： 这个文件存储的是 PaddlePaddle 模型的模型参数\n",
    "- demo.pdiparams.info ： 这个文件存储的是在使用模型 Fintune 时需要的额外的信息， 对于 Paddle2ONNX 项目来说，可以忽略。\n",
    "- demo.onnx ： 这个文件存储的是 ONNX 格式的模型（包括了模型结构和模型参数）。\n",
    "\n",
    "所以， Paddle2ONNX 所做的事情，就是把 demo.pdmodel + demo.pdiparams 转换成 demo.onnx 。\n",
    "\n",
    "BTW： Paddle2ONNX 项目，从 ONNX 的视角，是一个 [Converter](https://onnx.ai/onnx/intro/converters.html)。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31ae2456-dcbe-4292-83b0-6b564547cb09",
   "metadata": {
    "tags": []
   },
   "source": [
    "## 剥开这些模型文件看看\n",
    "\n",
    "我们可以使用如 [netron](https://netron.app/) 这样的工具来可视化出来 PaddlePaddle 模型和 ONNX 模型。 也可以使用代码加载文件然后看看这些模型文件里具体存储了哪些信息。\n",
    "\n",
    "### 我们先剥开 demo.pdmodel 文件看看。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "6dfd6ad2-10b6-4cc8-9268-c4d3e7e50660",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "blocks {\n",
      "  idx: 0\n",
      "  parent_idx: -1\n",
      "  vars {\n",
      "    name: \"linear_0.b_0\"\n",
      "    type {\n",
      "      type: LOD_TENS\n",
      "...\n",
      "sion: 1\n",
      "    }\n",
      "  }\n",
      "  pair {\n",
      "    op_name: \"affine_grid\"\n",
      "    op_version {\n",
      "      version: 1\n",
      "    }\n",
      "  }\n",
      "}\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import paddle.base.proto.framework_pb2 as pppb\n",
    "\n",
    "prog = pppb.ProgramDesc()\n",
    "with open('./demo.pdmodel', \"rb\") as f:\n",
    "    prog.ParseFromString(f.read())\n",
    "    \n",
    "print(str(prog)[:100] + '\\n...\\n' + str(prog)[-100:])\n",
    "# print(prog)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c99f51f3-888d-4852-97a1-2aa69d52398c",
   "metadata": {},
   "source": [
    "demo.pdmodel 文件的本质是一个根据 [framework.proto](https://github.com/PaddlePaddle/Paddle/blob/develop/paddle/fluid/framework/framework.proto) 的定义，使用 [protobuf](https://github.com/protocolbuffers/protobuf) 所自动生成的 `paddle.base.proto.framework_pb2`  进行解析，操作的持久化存储下来的模型结构信息。 因为 protobuf 所提供的能力， 使用其他编程语言，也同样可以解析和操作这个文件。 Paddle2ONNX 里，是使用 C++ 来解析这个文件， 具体进行解析这个文件的代码是：[PaddleParser](https://github.com/PaddlePaddle/Paddle2ONNX/blob/develop/paddle2onnx/parser/parser.cc) 。 \n",
    "\n",
    "[framework.proto](https://github.com/PaddlePaddle/Paddle/blob/develop/paddle/fluid/framework/framework.proto) 文件，可以理解为定义 PaddlePaddle 模型结构的 Schema， 所以 Paddle2ONNX 里也存储了一份， 是 [p2o_paddle.proto](https://github.com/PaddlePaddle/Paddle2ONNX/blob/develop/paddle2onnx/proto/p2o_paddle.proto) 。 这两个 proto 文件，除了 package name 外，其他部分是一模一样的。\n",
    "\n",
    "如果你有兴趣， 可以用 `paddle.base.proto.framework_pb2` 编辑，修改这个模型结构。你也可以用 [framework.proto](https://github.com/PaddlePaddle/Paddle/blob/develop/paddle/fluid/framework/framework.proto) 文件， 使用 protobuf，构建出来完全不需要 PaddlePaddle 和 Paddle2ONNX 的解析 PaddlePaddle 模型结构的工具。\n",
    "\n",
    "Some Fun: PaddlePaddle 模型里， 存储了 op_callstack， 那里记录了导出模型的机器上的文件路径，所以社区里会有[这样的 issue](https://github.com/PaddlePaddle/PaddleOCR/issues/11755)。\n",
    "\n",
    "### 剥开 demo.onnx 看看"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "7025a0cb-9818-4182-b648-25da02b90a7e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ir_version: 6\n",
      "graph {\n",
      "  node {\n",
      "    output: \"linear_0.b_0\"\n",
      "    op_type: \"Constant\"\n",
      "    attribute {\n",
      "  \n",
      "...\n",
      " dim_value: 1\n",
      "          }\n",
      "        }\n",
      "      }\n",
      "    }\n",
      "  }\n",
      "}\n",
      "opset_import {\n",
      "  domain: \"\"\n",
      "  version: 11\n",
      "}\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import onnx\n",
    "\n",
    "m = onnx.load('./demo.onnx')\n",
    "print(str(m)[:100] + '\\n...\\n' + str(m)[-100:])\n",
    "# print(m)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b466ba4b-af21-465a-b253-e2569eb3b14b",
   "metadata": {},
   "source": [
    "没错， demo.onnx 也是一个使用 protobuf 进行持久化存储下来的文件。 ONNX 项目规范了这个模型格式，让深度学习模型可以在不同的框架和引擎之间进行交换。 除了存储了模型结构之外， ONNX 格式的文件里也存储了模型的权重， 而 PaddlePaddle 的模型权重文件，是在 demo.pdiparams 文件里存储的。\n",
    "\n",
    "### 剥开 demo.pdiparams 看看\n",
    "\n",
    "PaddlePaddle 模型权重是用 PaddlePaddle 所定义的格式，以二进制的方式存储起来的。 但是存储格式，并没有详细的文档。只能通过阅读 Paddle2ONNX 的源码 [PaddleParser::LoadParams](https://github.com/PaddlePaddle/Paddle2ONNX/blob/develop/paddle2onnx/parser/parser.cc) 来了解其格式。 下面的代码， 用 python 实现了类似的功能。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "08dccdfa-08dd-43a4-bc94-be903c895ac6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "lod_level: 0 tensor_desc_size 4\n",
      "shape: [4]\n",
      "weight: [0. 0. 0. 0.]\n",
      "lod_level: 0 tensor_desc_size 6\n",
      "shape: [8, 4]\n",
      "weight: [ 0.37433714 -0.69927     0.55356663 -0.6761654   0.641123   -0.63947034\n",
      " -0.5111495  -0.26014766  0.00330591 -0.64222825 -0.07325768  0.14057362\n",
      "  0.18322802 -0.09095603  0.04319847 -0.69821215 -0.07351792 -0.18443882\n",
      " -0.00772464 -0.05234754 -0.52179754 -0.44880936  0.19592541  0.64138526\n",
      " -0.3487806   0.36517555 -0.28606617  0.46842593  0.17517066 -0.14055711\n",
      " -0.19386566  0.6340702 ]\n",
      "lod_level: 0 tensor_desc_size 4\n",
      "shape: [1]\n",
      "weight: [0.]\n",
      "lod_level: 0 tensor_desc_size 6\n",
      "shape: [4, 1]\n",
      "weight: [-0.42892092 -0.5319667  -0.26118833  0.09279704]\n"
     ]
    }
   ],
   "source": [
    "import struct \n",
    "import numpy as np\n",
    "\n",
    "with open('./demo.pdiparams', 'rb') as f:\n",
    "    raw_content = f.read()\n",
    "\n",
    "idx = 0\n",
    "while(idx < len(raw_content)):\n",
    "    magic_number1, lod_level, magic_number_2, tensor_desc_size = struct.unpack('=IQIi', raw_content[idx:idx+20])\n",
    "    print(f\"lod_level: {lod_level} tensor_desc_size {tensor_desc_size}\")\n",
    "    idx = idx + 20\n",
    "\n",
    "    tensor_desc = pppb.VarType.TensorDesc()\n",
    "    tensor_desc.ParseFromString(raw_content[idx:idx+tensor_desc_size])\n",
    "    idx = idx+tensor_desc_size\n",
    "    \n",
    "    numel = 1\n",
    "    for ele in tensor_desc.dims:\n",
    "        numel = numel * ele\n",
    "    \n",
    "    # 4 in next line because sizeof(float32)=4\n",
    "    weight = np.frombuffer(raw_content[idx:idx+numel*4], dtype=np.float32)\n",
    "    print(f\"shape: {tensor_desc.dims}\")\n",
    "    print(f\"weight: {weight}\")\n",
    "    idx = idx + numel * 4"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "91299212-62ff-4f50-8aa1-1a1e93c56a94",
   "metadata": {},
   "source": [
    "接下来，通过直接打印出来模型里的参数，我们可以验证出来，所存储在文件里的权重，跟直接打印出的模型参数是一样的。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "592c775c-1703-47a3-aac6-a39591e6f905",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Parameter containing:\n",
      "Tensor(shape=[4], dtype=float32, place=Place(cpu), stop_gradient=False,\n",
      "       [0., 0., 0., 0.])\n",
      "Parameter containing:\n",
      "Tensor(shape=[8, 4], dtype=float32, place=Place(cpu), stop_gradient=False,\n",
      "       [[ 0.37433714, -0.69927001,  0.55356663, -0.67616540],\n",
      "        [ 0.64112300, -0.63947034, -0.51114953, -0.26014766],\n",
      "        [ 0.00330591, -0.64222825, -0.07325768,  0.14057362],\n",
      "        [ 0.18322802, -0.09095603,  0.04319847, -0.69821215],\n",
      "        [-0.07351792, -0.18443882, -0.00772464, -0.05234754],\n",
      "        [-0.52179754, -0.44880936,  0.19592541,  0.64138526],\n",
      "        [-0.34878060,  0.36517555, -0.28606617,  0.46842593],\n",
      "        [ 0.17517066, -0.14055711, -0.19386566,  0.63407022]])\n",
      "Parameter containing:\n",
      "Tensor(shape=[1], dtype=float32, place=Place(cpu), stop_gradient=False,\n",
      "       [0.])\n",
      "Parameter containing:\n",
      "Tensor(shape=[4, 1], dtype=float32, place=Place(cpu), stop_gradient=False,\n",
      "       [[-0.42892092],\n",
      "        [-0.53196669],\n",
      "        [-0.26118833],\n",
      "        [ 0.09279704]])\n"
     ]
    }
   ],
   "source": [
    "print(model.linear1.bias)\n",
    "print(model.linear1.weight)\n",
    "print(model.linear2.bias)\n",
    "print(model.linear2.weight)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2e8a7289-f24b-4de7-8dec-87ce938cf133",
   "metadata": {},
   "source": [
    "一些额外的说明：\n",
    "\n",
    "- 所存储的模型权重跟模型结构里的 Variable 的对应关系： 如果查看 `prog.blocks[0].vars` 的话，你会发现，其数量是大于存储的 4 个模型权重的，因为 `vars` 还包括了其他的参与计算的 Variable。 他们对应的关系的建立的逻辑是按照 `prog.blocks[0].vars` 的顺序，挑选出被认定为模型权重的 Variable 。这部分在 Paddle2ONNX 里的实现是在 [PaddleParser::GetParamNames](https://github.com/PaddlePaddle/Paddle2ONNX/blob/develop/paddle2onnx/parser/parser.cc)。 坦白说，如果能在存储模型时建立起来权重跟 Variable 的对应关系，会更容易理解。这方面， ONNX 的模型文件就非常容易理解。\n",
    "- LoD 是什么： LoD 是 Level of Details 的缩写，这是飞桨最开始设计里的一个特色，可以方便的表达不定长的 tensor 组成的 batch， 跟 Tensorflow 里的 [Ragged Tensor](https://www.tensorflow.org/guide/ragged_tensor)，或者 pytorch 里的 [nested tensor](https://pytorch.org/docs/stable/nested.html) 是类似的。 Paddle2ONNX 目前只能处理 `lod_level = 0` 的情况，当 `lod_level != 0` 时，会直接解析失败。\n",
    "- magic＿number1, magic_number2 是什么： 这是存储在模型权重文件里的两个神秘数字（版本？）， Paddle2ONNX 里用不到。\n",
    "- 权重的精度：模型权重文件里读到的 tensor_desc 里记录了数据类型（FP32， FP16， 等等）。\n",
    "- block 又是什么：如果你把飞桨的模型结构当成一段计算机程序，那么每个 block 可以理解为一段程序代码。 当程序中有条件判断时，就会有多个 block。 现在， Paddle2ONNX 只能处理 Program 中仅有 1 个 block 的情况。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "55ed7ae0-88e2-4924-aa93-fa685b95c4d3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ">>> all variables:\n",
      "linear_0.b_0, True\n",
      "linear_0.tmp_0, False\n",
      "linear_0.tmp_1, False\n",
      "linear_0.w_0, True\n",
      "linear_1.b_0, True\n",
      "linear_1.tmp_0, False\n",
      "linear_1.tmp_1, False\n",
      "linear_1.w_0, True\n",
      "save_infer_model/scale_0.tmp_0, False\n",
      "sigmoid_0.tmp_0, False\n",
      "x, False\n",
      "feed, True\n",
      "fetch, True\n",
      ">>> all ops:\n",
      "feed\n",
      "matmul_v2\n",
      "elementwise_add\n",
      "sigmoid\n",
      "matmul_v2\n",
      "elementwise_add\n",
      "scale\n",
      "fetch\n"
     ]
    }
   ],
   "source": [
    "print('>>> all variables:')\n",
    "print('\\n'.join([f\"{x.name}, {x.persistable}\" for x in prog.blocks[0].vars]))\n",
    "print('>>> all ops:')\n",
    "print('\\n'.join([op.type for op in prog.blocks[0].ops]))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61ecf390-7b1c-4f33-80bb-6cbc15e6a19c",
   "metadata": {},
   "source": [
    "## 小讨论\n",
    "\n",
    "本文前面的部分，我们把 PaddlePaddle 模型的格式跟 ONNX 模型的格式都概要的通过代码的形式展示了出来。按照一般的做格式转换的项目的基本流程（例如，把 jpg 文件转换成 png 文件），基本的做法，应该就是先解析源格式的文件，然后根据解析得到的信息，用目标格式重新构建文件，并存储。看起来并不复杂，就这篇文章里作为示例用的 demo 模型来说，再稍进一步，已经可以用 python 构建出来一个 ONNX 模型了。 但对于要能把所有 PaddlePaddle 模型转换成 ONNX 模型格式的 Paddle2ONNX 项目来说，面临着如下的重要挑战：\n",
    "\n",
    "1. 模型格式的演进： 首先是 PaddlePaddle 模型格式的演进，新的 PaddlePaddle 的版本的发布，都潜在的会有新的模型格式的变化，即将发布的飞桨 3.0 版本， 因为 PIR 的正式上线，会带来不少的变化。这对于 Paddle2ONNX 项目来说，是需要迭代支持的。 其次是 ONNX 模型格式的演进， 每年发布 3~5 个 release 的 ONNX 的模型格式也会有新的变化。 作为一个 Converter 的 Paddle2ONNX 需要能够跟上节奏（keep up the rhythm）。\n",
    "1. OP Coverage ： PaddlePaddle 模型里的每一个 OP 都需要语义正确的，转换为 ONNX 格式里的表示，这是一个相当大的工作量。\n",
    "1. 控制流，不规则张量，混合精度计算，模型优化，自定义OP，量化，等等情况的支持。\n",
    "\n",
    "\n",
    "BTW：Paddle2ONNX 项目曾经是完全用 python 来实现的， 例如： [v0.9](https://github.com/PaddlePaddle/Paddle2ONNX/tree/v0.9)。 但是出于能够更好地解决边界情况的原因，项目组后来重构成了用 C++ 作为其核心来实现。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "afd068e6-233e-49f6-9030-b6e45ecbc02a",
   "metadata": {
    "tags": []
   },
   "source": [
    "## 构建 ONNX 模型\n",
    "\n",
    "ONNX 对于深度学习模型的抽象比较容易理解。就是一个由节点（Node），构成的图（Graph）。图有输入（Input）和输出（Output）。每个节点（Node）也有输入（Input）和输出（Output），以及代表对输入进行计算得到输出的计算方式的算子（Operator），和固定的属性（Attribute）。更详细的介绍可以参考[ONNX Concept](https://onnx.ai/onnx/intro/concepts.html)。\n",
    "\n",
    "下面是构建出来跟使用 paddle2onnx 转换出来的 `demo.onnx` 等价的 ONNX 模型的代码。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "a774d7cd-031f-4eb7-be33-dbb315129648",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "graph demograph (\n",
      "  %x[FLOAT, ?x8]\n",
      ") {\n",
      "  %A = Constant[value = <Tensor>]()\n",
      "  %A_bias = Constant[value = <Tensor>]()\n",
      "  %B = Constant[value = <Tensor>]()\n",
      "  %B_bias = Constant[value = <Tensor>]()\n",
      "  %XA = MatMul(%x, %A)\n",
      "  %linear1_out = Add(%XA, %A_bias)\n",
      "  %sigmoid = Sigmoid(%linear1_out)\n",
      "  %sigmoidB = MatMul(%sigmoid, %B)\n",
      "  %y = Add(%sigmoidB, %B_bias)\n",
      "  return %y\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "from onnx import TensorProto\n",
    "from onnx.helper import (\n",
    "        make_model, make_node, make_graph,\n",
    "        make_tensor_value_info)\n",
    "from onnx.checker import check_model\n",
    "\n",
    "x = make_tensor_value_info('x', TensorProto.FLOAT, [None, input_size])\n",
    "y = make_tensor_value_info('y', TensorProto.FLOAT, [None])\n",
    "\n",
    "weights = {'A':model.linear1.weight,\n",
    "        'A_bias':model.linear1.bias,\n",
    "        'B':model.linear2.weight,\n",
    "        'B_bias':model.linear2.bias}\n",
    "\n",
    "weight_nodes = []\n",
    "\n",
    "for name,weight in weights.items():\n",
    "    n =  make_node(\n",
    "            \"Constant\", [], [name],\n",
    "            value=onnx.helper.make_tensor(\n",
    "                name=name,\n",
    "                data_type=onnx.TensorProto.FLOAT,\n",
    "                dims=weight.shape,\n",
    "                vals=weight.numpy().flatten(),\n",
    "                )\n",
    "            )\n",
    "    weight_nodes.append(n)\n",
    "\n",
    "node1 = make_node('MatMul', ['x', 'A'], ['XA'])\n",
    "node2 = make_node('Add', ['XA', 'A_bias'], ['linear1_out'])\n",
    "node3 = make_node('Sigmoid', ['linear1_out'], ['sigmoid'])\n",
    "node4 = make_node('MatMul', ['sigmoid', 'B'], ['sigmoidB'])\n",
    "node5 = make_node('Add', ['sigmoidB', 'B_bias'], ['y'])\n",
    "\n",
    "graph = make_graph(weight_nodes + [node1, node2, node3, node4, node5],  # nodes\n",
    "                    'demograph',  # a name\n",
    "                    [x],  # inputs\n",
    "                    [y])  # outputs\n",
    "\n",
    "onnx_model = make_model(graph)\n",
    "check_model(onnx_model)\n",
    "print(onnx.helper.printable_graph(onnx_model.graph))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4fc00bd0-6502-4ff4-8e80-0daaf8120b41",
   "metadata": {},
   "source": [
    "飞桨模型的结构里尽管有 Program、Block、Variable、Operator 等等的概念，但其本质上也是一个图（Graph）。构建 ONNX 模型的关键在于基于从飞桨模型当中找到的参数，算子，输入输出，然后使用 ONNX 提供的 API 构建出来与飞桨模型等价的图（Graph）。\n",
    "在 Paddle2ONNX 项目中，构建 ONNX 模型的代码的实现在这里： [ModelExporter::Run](https://github.com/PaddlePaddle/Paddle2ONNX/blob/develop/paddle2onnx/mapper/exporter.cc)。我们打印出来使用 paddl2onnx 转换出来的示例的模型结构可以看到，跟上面的用 python 构建的 ONNX 的图是一样的。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "370eae39-ac38-403d-8029-a7dbbe41737c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ">>> onnx graph converted using paddle2onnx :\n",
      "graph Model from PaddlePaddle. (\n",
      "  %x[FLOAT, p2o.DynamicDimension.0x8]\n",
      ") {\n",
      "  %linear_0.b_0 = Constant[value = <Tensor>]()\n",
      "  %linear_0.w_0 = Constant[value = <Tensor>]()\n",
      "  %linear_1.b_0 = Constant[value = <Tensor>]()\n",
      "  %linear_1.w_0 = Constant[value = <Tensor>]()\n",
      "  %p2o.MatMul.1 = MatMul(%x, %linear_0.w_0)\n",
      "  %p2o.Add.1 = Add(%p2o.MatMul.1, %linear_0.b_0)\n",
      "  %sigmoid_0.tmp_0 = Sigmoid(%p2o.Add.1)\n",
      "  %p2o.MatMul.3 = MatMul(%sigmoid_0.tmp_0, %linear_1.w_0)\n",
      "  %save_infer_model/scale_0.tmp_0 = Add(%p2o.MatMul.3, %linear_1.b_0)\n",
      "  return %save_infer_model/scale_0.tmp_0\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "print('>>> onnx graph converted using paddle2onnx :')\n",
    "print(onnx.helper.printable_graph(m.graph))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fcd57329-e2ee-473a-999e-0cc6bc7b0111",
   "metadata": {},
   "source": [
    "## OP Mapper\n",
    "\n",
    "构建 ONNX 模型中的输入，输出和参数（weight），对于每个模型来说，都是固定的转换流程。对于 Paddle2ONNX 来说，工作量最大的部分在于如何把飞桨模型里的算子，对应到等价的 ONNX 算子里的一个或多个算子的组合上，确保语义是等价的，计算结果是能匹配上的。Paddle2ONNX 已经搭建好了脚手架来更方便的做这个工作。当需要新增一个算子支持时，只需要关注添加对应的 OP Mapper 就可以了。\n",
    "\n",
    "以示例里的模型里的 `matmul_v2` 这个飞桨的算子为例，它会经过 [MatmulV2Mapper](https://github.com/PaddlePaddle/Paddle2ONNX/blob/develop/paddle2onnx/mapper/tensor/matmul_v2.h) 被转换成 `MatMul` 和 `Identity` 两个算子。（其中  `Identity` 这个算子是因为做自动的精度转换而添加的）； sigmoid 这个飞桨的算子，会经过 [ActivationMapper](https://github.com/PaddlePaddle/Paddle2ONNX/blob/develop/paddle2onnx/mapper/activation.h) 被转换成 `Sigmoid` 这个 ONNX 算子。关于实际的添加 OP Mapper 的方法，可以参看文档：[Paddle2ONNX_Development_Guide.md](https://github.com/PaddlePaddle/Paddle2ONNX/blob/develop/docs/zh/Paddle2ONNX_Development_Guide.md)，本文就不再赘述了。\n",
    "\n",
    "\n",
    "BTW：你可以用 [MapperHelper::GetAllOps](https://github.com/PaddlePaddle/Paddle2ONNX/blob/develop/paddle2onnx/mapper/register_mapper.h)，来导出，所有已经支持的飞桨算子的列表。（需要找时间，把这个 API 导出到 python 端，会更加方便一些）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b55192b0-888b-4365-8986-fb9822878722",
   "metadata": {},
   "source": [
    "## Debug Tricks & Misc\n",
    "- 建议使用 `pip install -e .` 来以 editable mode 在本地编译和开发 Paddle2ONNX，这样可以比较方便的进行迭代开发。\n",
    "- 可以在调试时使用 `P2OLogger` 来打印日志，来观察实际的运行过程。\n",
    "- cmake 里有一个编译选项 `PADDLE2ONNX_DEBUG`，目前只起到了非常小的作用，也许需要有人能真正把这个编译选项下的日志等等规范起来，来方便 Paddle2ONNX 的开发。\n",
    "- 代码仓库里目前还没有真正利用上 linting 等工具，也许需要有人来投入精力做一下。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5c81970d-d397-4720-ac7f-f206d89bfa8e",
   "metadata": {},
   "source": [
    "## Ending\n",
    "\n",
    "这篇文章是基于粗浅的对 Paddle2ONNX 的理解完成的，希望对开发 Paddle2ONNX 能有帮助。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.19"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
